// TODO: CODE REVIEW & CLEANUP!
//
// ISyncClient.cs
//
// Defines ISyncClient and related types.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Xml;
using HomeUX.Networking;
using HomeUX.Utilities;
#if SILVERLIGHT
using System.Windows;
using System.Windows.Threading;
using HomeUX.ClientControlSupport;
#endif

namespace HomeUX.Synchronization
{

/// <summary>
/// TODO
/// </summary>
///
public interface ISyncClient
{
    SyncNodeBase SyncTree { get; }
    bool IsOnline { get; }
    event IsOnlineChangedEventHandler IsOnlineChanged;
    //void ConnectToServer();
    void QueueSyncNodeToSendToServer(SyncNodeBase node);
    void QueueSyncNodeCommandToSendToServer(SyncNodeBase node, string verb);
    void QueueDescendantChangedEvent(SyncNodeBase syncNode, DescendantChanges changes, object originator);
}

// represents the path of a SyncNode; provides helper functionality; immutable!
// A SyncPath object represents a "normalized" SyncPath -- for example, SyncPath.FromString(@"a\b\..\c")
// yields a SyncPath object that contains the path "a\c".  SyncPath.FromString("..\..\.\a\.") results in
// the SyncPath object with one part, "a", and UpCount equal to 2, which represents the path "..\..\a" --
// and in fact if you call ToString() on that SyncPath that's the string that would be returned.
// SyncPath "." and "" (String.Empty) both mean the same thing, i.e. the current SyncNode (if used in a
// relative manner) or the root node (if used in an absolute manner), and both are equivalent to SyncNode.Empty.

/// <summary>
/// TODO -- see comments above.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay}")]
public class SyncPath
{
    string[] _parts;

	static readonly string[] _noParts = new string[0];

    static readonly SyncPath _empty = new SyncPath { _parts = _noParts };

    // refers to either the root (in the context of an absolute SyncPath) or the current node (in
    // the context of a relative SyncPath)
    public static SyncPath Empty
    {
        get
        {
            return _empty;
        }
    }

    /// <summary>
    /// Returns <n>true</n> if this path is "Personalization" or begins with "Personalization\".
    /// </summary>
    public bool IsPersonalization
    {
        get
        {
            return (_parts.Length >= 1) && (_parts[0] == "Personalization");
        }
    }

    public override string ToString()
    {
        // set <path> to the string form of <_parts>
        string path;
        if (_parts.Length == 0)
            path = String.Empty;
        else
            path = String.Join(@"\", _parts);

        if (UpCount == 0)
        {
            // SyncPath has no leading ".." components
            return path;
        }
        else
        {
            var prefix = new StringBuilder(3/*"..\".Length*/ * UpCount);
            for (int i = UpCount; i > 0; i--)
                prefix.Append(@"..\");

            string prefixString = prefix.ToString();
            if (path.Length > 0)
            {
                // e.g. SyncPath is "..\..\a\b\c"
                return prefixString + path;
            }
            else
            {
                // e.g. SyncPath is "..\.." -- remove trailing "\"
                return prefixString.Substring(0, prefixString.Length - 1);
            }
        }
    }

    public string[] Parts
    {
        get
        {
            return _parts;
        }
    }

    public int UpCount { get; private set; }

    public void EnsureZeroUpCount()
    {
        if (UpCount != 0)
            throw new SyncException(this, "A path beginning with \"..\" is not valid in this context");
    }

    // constructs a SyncPath from a string containing path parts separated by backslashes;
    // "." and ".." within parts are normalized;
    // if null is passed in, null is returned
    public static SyncPath FromString(string pathAsString)
    {
        if (pathAsString == null)
            return null; // i.e. no sync path

        string[] parts = ParseParts(pathAsString);

        if (parts.Length == 0)
            return SyncPath.Empty; // i.e. root or "." (current node)

        return SyncPath.Build(parts);
    }

    static string[] ParseParts(string pathAsString)
    {
        return pathAsString.Split(new char[] { '\\' },
            StringSplitOptions.RemoveEmptyEntries);
    }

    // each part can also be a backslash-separated list of parts, e.g. SyncPath.Build("a", @"b\c\d")
    // returns the same value as SyncPath.FromString(@"a\b\c\d")
    // if null is passed in, null is returned
    public static SyncPath Build(params string[] parts)
    {
        if (parts == null)
            return null;

        // handle the case where one or more elements of <parts> contains a backslash -- in this
        // case, convert each such element to an array of "subparts", and use Build(object[])
        List<object> args = null;
		foreach (string part in parts)
		{
            if (part.Contains(@"\"))
			{
				args = new List<object>(parts.Length + 10/*hint*/);
				break;
			}
		}
		if (args != null)
		{
            foreach (string part in parts)
            {
				if (part.Contains(@"\"))
				{
					string[] subParts = ParseParts(part);
					args.Add(subParts);
				}
				else
					args.Add(part);
			}
			return Build(args.ToArray());
		}

        var syncPath = new SyncPath()
        {
            _parts = parts
        };

        // Adjust <this> so it doesn't contain "." and ".."; example:
        // "a\b\..\..\..\c" --> "..\c"

        int nonDotIndex = -1; // <parts> index of first part that's not "." or ".."; -1 if none
        List<string> mergedParts = null;
        for (int index = 0; index < parts.Length; index++)
        {
            string part = parts[index];
            bool isDot = (part == ".");
            bool isDotDot = (part == "..");
            if (isDot || isDotDot)
            {
                if (nonDotIndex < 0)
                {
                    // we're within one or more "."/".." parts at the beginning of the path
                    if (isDotDot)
                        syncPath.UpCount++;
                }
                else
                {
                    // we found a "." or ".." inside the path, past a non-"."/".." part...

                    // initialize <mergedParts> if it wasn't initialized yet -- it will contain
                    // <parts> but without the leading "."/".." parts and with embedded "."/".."
                    // merged, e.g. "..\..\a\b\c\..\d\..\..\e" becomes "a\e" (with UpCount = 2)
                    if (mergedParts == null)
                    {
                        mergedParts = new List<string>(parts.Length);
                        for (int i = nonDotIndex; i < index; i++)
                            mergedParts.Add(parts[i]);
                    }

                    if (isDotDot)
                    {
                        if (mergedParts.Count > 0)
                            mergedParts.RemoveAt(mergedParts.Count - 1);
                        else
                            syncPath.UpCount++;
                    }
                }
            }
            else
            if (nonDotIndex < 0)
                nonDotIndex = index;
            else
            if (mergedParts != null)
                mergedParts.Add(part);
        }
        if (mergedParts != null)
        {
            // e.g. "a\b\..\c\d" -- replace <_parts> with <mergedParts>
            syncPath._parts = mergedParts.ToArray();
        }
        else
        if (nonDotIndex > 0)
        {
            // e.g. "..\..\a\b\c" -- remove leading "."/".." (since they're now incorporated into
            // UpCount)
            int length = syncPath._parts.Length - nonDotIndex;
            if (length > 0)
                Array.Copy(syncPath._parts, nonDotIndex, syncPath._parts, 0, length);
        }
        else
        if (nonDotIndex < 0)
        {
            // e.g. "." or "..\.." -- remove all parts (since they're now incorporated into
            // UpCount)
            syncPath._parts = _noParts;
        }

        return syncPath;
    }

    // each arg can be: a string (path part, or multiple parts separated by backslashes),
	// a string array (a bunch of path parts), an object array (the same thing you'd pass to
	// Build as an argument), or a SyncPath
    // Example
    /*
        var path1 = SyncPath.FromString(@"a\..\..\b\.\c\.");
        var path2 = SyncPath.Build("q", new string[] { @"r\\s\..\t\u", @"..\v\w\x" }, path1);

        path1 is the SyncPath "..\b\c", i.e. two parts "b" and "c", with UpCount = 1
        path2 is the SyncPath "r\t\v\w\b\c", i.e. six parts with UpCount = 0 

     */
    public static SyncPath Build(params object[] args)
    {
        int count = 0;
        int index = 0;
        string[] parts = null;
        for (int pass = 1; pass <= 2; pass++)
        {
            foreach (object rawArg in args)
            {
                object arg;
                object[] objectArrayArg = rawArg as object[];
                if (objectArrayArg != null)
                    arg = Build(objectArrayArg);
                else
                    arg = rawArg;

                string stringArg;
                string[] stringArrayArg;
                SyncPath syncPath = null;
                if (arg == null)
                {
                    // ignore it
                }
                else
                if ((stringArg = arg as string) != null)
                {
                    if (parts == null) // pass 1
                        count++;
                    else // pass 2
                        parts[index++] = stringArg;
                }
                else
                if (((stringArrayArg = arg as string[]) != null) ||
                    (((syncPath = arg as SyncPath) != null) &&
					 ((stringArrayArg = syncPath.Parts) != null)))
                {
                    int upCount = ((syncPath != null) ? syncPath.UpCount : 0);
                    if (parts == null) // pass 1
                        count += stringArrayArg.Length + upCount;
                    else
                    {
                        for (int i = 0; i < upCount; i++)
                            parts[index++] = "..";
                        Array.Copy(stringArrayArg, 0, parts, index, stringArrayArg.Length);
                        index += stringArrayArg.Length + upCount;
                    }
                }
                else
                {
                    throw new ArgumentException(String.Format(
                        "SyncPath.Build cannot accept an argument of type {0}", arg.GetType()));
                }
            }

            if (pass == 1)
                parts = new string[count];
            else
                break;
        }

        return SyncPath.Build(parts);
    }

    // error if path is too short for <index>
    public string GetRequiredPart(int index)
    {
        if (index >= _parts.Length)
            throw new SyncException(this, "Path is too short");
        return _parts[index];
    }

    // error if <index> isn't the last part
    public string GetRequiredLastPart(int index)
    {
        VerifyPathLength(index + 1);
        return _parts[index];
    }

    public void VerifyPathLength(int length)
    {
        if (_parts.Length != length)
        {
            throw new SyncException(this,
                "Expecting path to have {0} part(s), but it has {1} part(s)",
                length, _parts.Length);
        }
    }

    // gets sync path part <index>, matches against each string in <options>; throws an exception
    // if no such part, or if part doesn't match <options>
    public int MatchRequiredPart(int index, params string[] options)
    {
        string part = GetRequiredPart(index);
        for (int i = 0; i < options.Length; i++)
        {
            if (part == options[i])
                return i;
        }
        throw new SyncException(this, "\"{0}\" is not valid; expecting one of: ",
            String.Join(", ", options));
    }

    public override bool Equals(object obj)
    {
        var other = obj as SyncPath;
        if (other == null)
            return false;

        if (Parts.Length != other.Parts.Length)
            return false;

        if (UpCount != other.UpCount)
            return false;

        for (int index = 0; index < _parts.Length; index++)
        {
            if (_parts[index] != other._parts[index])
                return false;
        }

        return true;
    }

    public override int GetHashCode()
    {
        int hashCode = ~UpCount;
		foreach (string part in _parts)
			hashCode ^= part.GetHashCode();
        return hashCode;
    }

    public static bool operator ==(SyncPath x, SyncPath y)
    {
        if ((object)x == null)
            return ((object)y == null);
        else
            return x.Equals(y);
    }

    public static bool operator !=(SyncPath x, SyncPath y)
    {
        return !(x == y);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Properties
    //

    /// <summary>
    /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
    /// from displaying it in quotes.
    /// </summary>
    object DebuggerDisplay
    {
        get
        {
            return new EncapsulatedString(ToString());
        }
    }
}

/// <summary>
/// TODO
/// </summary>
public abstract class SyncNodeOperation
{
    public SyncPath Path { get; set; }

    /// <summary>
    /// For a <r>SyncNodeOperation</r> sent from the server to a client, <r>OriginatedHere</r> is
    /// the count of <r>SyncNodeOperation</r> messages from that client for which this
    /// <r>SyncNodeOperation</r> is a response.
    /// </summary>
    ///
    /// <remarks>
    /// For example, suppose the user drags a volume slider up, generating a sequence of 5
    /// <r>SyncNodeUpdate</r> messages that are sent from the client to the server.  The client
    /// will display an hourglass until it gets response(s) from the server indicating that all 5
    /// message have been processed.  The response(s) could be 5 separate <r>SyncNodeUpdate</r>
    /// messages with <r>OriginatedHere</r> = 1, or 1 <r>SyncNodeUpdate</r> message with
    /// <r>OriginatedHere</r> = 5 (if the 5 messages were collapsed into one single action), or
    /// something in-between.
    /// </remarks>
    ///
    public int OriginatedHere { get; set; }

    public abstract void Serialize(XmlWriter writer, bool fromClient);
}

/// <summary>
/// TODO
/// </summary>
[DebuggerDisplay("{DebuggerDisplay}")]
public class SyncNodeUpdate : SyncNodeOperation
{
    /// <summary>
    /// The new value for the <r>SyncNode</r>.
    /// </summary>
    /// 
    /// <remarks>
    /// <r>Value</r> will be <r>Message.FailedValue</r> for a <r>SyncNodeUpdate</r> sent from the
    /// server to a client in response to a client's <r>SyncNodeUpdate</r>, to indicate that the
    /// update failed.
    /// </remarks>
    ///
    public object Value { get; set; }

    /// <summary>
    /// Returns <n>true</n> if <r>Value</r> is <r>Message.FailedValue</r>.  For a
    /// <r>SyncNodeUpdate</r> sent from the server to a client in response to a client's
    /// <r>SyncNodeUpdate</r>, this indicates that the update failed.
    /// </summary>
    public bool IsFailure
    {
        get
        {
            return Value == Message.FailedValue;
        }
    }

    // throws MessageProtocolException if value cannot be serialized;
    // <fromClient> is true if the resulting message will be sent from client to server
    public override void Serialize(XmlWriter writer, bool fromClient)
    {
        // set <valueString> to the value to use for the "Value" attribute; set <typeString> to
        // the value to use for the "Type" attribute or null for no "Type" attribute (which means
        // that the type is System.String); the code below is what may throw an
        // ArgumentException -- we do it here, before writing to <writer>
		string valueString, typeString;
		SerializeValue(Value, out valueString, out typeString);
		/*
        string valueString = Value as string;
        string typeString;
        try
        {
            if (valueString == null)
            {
                TypeCode typeCode;
                valueString = Message.ObjectToXmlString(Value, out typeCode);
                typeString = XmlConvert.ToString((int)typeCode);
            }
            else
                typeString = null;
        }
        catch (ArgumentException ex)
        {
            throw new MessageProtocolException(ex, "SyncNodeUpdate.Serialize failed");
        }
		*/

        // serialize the value
        writer.WriteStartElement("SyncNodeUpdate");
        writer.WriteAttributeString("Path", Path.ToString());
        if (!String.IsNullOrEmpty(valueString))
            writer.WriteAttributeString("Value", valueString);
        if (typeString != null)
            writer.WriteAttributeString("Type", typeString);
        if ((OriginatedHere > 0) && !fromClient)
            writer.WriteAttributeString("OriginatedHere", XmlConvert.ToString(OriginatedHere));
        writer.WriteEndElement();
    }

    // throws MessageProtocolException if value cannot be serialized
    public static void SerializeValue(object value, XmlWriter writer)
    {
        string valueString, typeString;
        SerializeValue(value, out valueString, out typeString);
        if (!String.IsNullOrEmpty(valueString))
            writer.WriteAttributeString("Value", valueString);
        if (typeString != null)
            writer.WriteAttributeString("Type", typeString);
    }

    // throws MessageProtocolException if value cannot be serialized
	public static void SerializeValue(object value, out string valueString, out string typeString)
	{
        valueString = value as string;
        try
        {
            if (valueString == null)
            {
                TypeCode typeCode;
                valueString = Message.ObjectToXmlString(value, out typeCode);
                typeString = XmlConvert.ToString((int)typeCode);
            }
            else
                typeString = null;
        }
        catch (ArgumentException ex)
        {
            throw new MessageProtocolException(ex, "SyncNodeUpdate.Serialize failed");
        }
	}

    // throws MessageProtocolException if a SyncNodeUpdate cannot be deserialized
    public static SyncNodeUpdate Deserialize(XmlReader reader)
    {
        try
        {
            var update = new SyncNodeUpdate();

            string path = reader.GetAttribute("Path");
            if (path == null)
                throw new MessageProtocolException(null, "Missing Path attribute");
            try
            {
                update.Path = SyncPath.FromString(path);
            }
            catch (SyncException ex)
            { 
                throw new MessageProtocolException(ex, "Invalid Path attribute: {0}", path);
            }

			/*
            string valueString = reader.GetAttribute("Value");
            if (valueString == null)
                valueString = String.Empty;

            string typeString = reader.GetAttribute("Type");
            if (typeString == null)
            {
                // type defaults to System.String
                update.Value = valueString;
            }
            else
            {
                TypeCode typeCode = (TypeCode)XmlConvert.ToInt32(typeString);
                update.Value = Message.XmlStringToObject(valueString, typeCode);
            }
			*/

			update.Value = DeserializeValue(reader);

            string originatedHereString = reader.GetAttribute("OriginatedHere");
            if (originatedHereString != null)
                update.OriginatedHere = XmlConvert.ToInt32(originatedHereString);

            return update;
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException) ||
                (ex is ArgumentException) || (ex is MessageProtocolException))
            {
                throw new MessageProtocolException(ex, "SyncNodeUpdate.Deserialize failed");
            }
            else
                throw;
        }
    }

    // throws MessageProtocolException if a SyncNodeUpdate cannot be deserialized
    public static object DeserializeValue(XmlReader reader)
	{
		try
		{
			string valueString = reader.GetAttribute("Value");
			if (valueString == null)
				valueString = String.Empty;

			string typeString = reader.GetAttribute("Type");
			if (typeString == null)
			{
				// type defaults to System.String
				return valueString;
			}
			else
			{
				TypeCode typeCode = (TypeCode)XmlConvert.ToInt32(typeString);
				return Message.XmlStringToObject(valueString, typeCode);
			}
		}
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException) ||
                (ex is ArgumentException))
            {
                throw new MessageProtocolException(ex, "SyncNodeUpdate.DeserializeValue failed");
            }
            else
                throw;
        }
	}

    public T GetTypedValue<T>()
    {
        if (Value == null)
            throw new SyncException("{0}Expected non-null value", ExceptionMessagePrefix);

        try
        {
            if (Value is T)
                return (T)Value;

            string stringValue = Value as string;
            if (stringValue == null)
                throw new InvalidCastException();

            // the following code is primarily to allow for scripts specifying e.g.
            // "property = 0.7" where the property isn't an aggregator property -- in this case,
            // the "0.7" is a string but needs to be converted to a double; same idea for booleans
            if (typeof(T) == typeof(double))
                return (T)(object)XmlConvert.ToDouble(stringValue);
            else
            if (typeof(T) == typeof(bool))
                return (T)(object)XmlConvert.ToBoolean(stringValue);
            else
                throw new InvalidCastException();
        }
        catch (Exception ex)
        {
            if ((ex is InvalidCastException) || (ex is FormatException) || (ex is OverflowException))
            {
                throw new SyncException(
                    "{0}Cannot convert value \"{1}\" from type {2} to type {3}",
                    ExceptionMessagePrefix, Value.ToString(), Value.GetType().Name, typeof(T).Name);
            }
            else
                throw;
        }
    }

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    string ExceptionMessagePrefix
    {
        get
        {
            if (Path != null)
                return String.Format("SyncNodeUpdate Path=\"{0}\": ", Path);
            else
                return "SyncNodeUpdate: ";
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Properties
    //

    /// <summary>
    /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
    /// from displaying it in quotes.
    /// </summary>
    object DebuggerDisplay
    {
        get
        {
            return new EncapsulatedString("SyncNodeUpdate \"{0}\" -> {1}", Path, Value);
        }
    }
}

/// <summary>
/// TODO - a command consists of a SyncPath + a verb...
/// </summary>
[DebuggerDisplay("{DebuggerDisplay}")]
public class SyncNodeCommand : SyncNodeOperation
{
    public string Verb { get; set; }
    // throws MessageProtocolException if value cannot be serialized;
    // <fromClient> is true if the resulting message will be sent from client to server

    public override void Serialize(XmlWriter writer, bool fromClient)
    {
        if (String.IsNullOrEmpty(Verb))
            throw new InvalidOperationException("Cannot serialize SyncNodeCommand if Verb is null or empty");

        writer.WriteStartElement("SyncNodeCommand");
        writer.WriteAttributeString("Path", Path.ToString());
        writer.WriteAttributeString("Verb", Verb);
        if ((OriginatedHere > 0) && !fromClient)
            writer.WriteAttributeString("OriginatedHere", XmlConvert.ToString(OriginatedHere));
        writer.WriteEndElement();
    }

    // throws MessageProtocolException if a SyncNodeCommand cannot be deserialized
    public static SyncNodeCommand Deserialize(XmlReader reader)
    {
        try
        {
            var command = new SyncNodeCommand();

            string path = reader.GetAttribute("Path");
            if (path == null)
                throw new MessageProtocolException(null, "Missing Path attribute");
            try
            {
                command.Path = SyncPath.FromString(path);
            }
            catch (SyncException ex)
            {
                throw new MessageProtocolException(ex, "Invalid Path attribute: {0}", path);
            }

            command.Verb = reader.GetAttribute("Verb");
            if (command.Verb == null)
                throw new MessageProtocolException(null, "Missing Verb attribute");

            string originatedHereString = reader.GetAttribute("OriginatedHere");
            if (originatedHereString != null)
                command.OriginatedHere = XmlConvert.ToInt32(originatedHereString);

            return command;
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException) ||
                (ex is ArgumentException) || (ex is MessageProtocolException))
            {
                throw new MessageProtocolException(ex, "SyncNodeCommand.Deserialize failed");
            }
            else
                throw;
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Properties
    //

    /// <summary>
    /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
    /// from displaying it in quotes.
    /// </summary>
    object DebuggerDisplay
    {
        get
        {
            return new EncapsulatedString("SyncNodeCommand \"{0}\" : {1}", Path, Verb);
        }
    }
}

/// <summary>
/// A node in the global SyncTree.
/// </summary>
///
/// <remarks>
/// <para>
/// A SyncTree node contains a <s>SyncNodeBase.Name</s>, a <r>Value</r> (which is either <n>null</n> or of one
/// of the types specified in <r>SyncNodeValueType</r>) and may also contain one or more
/// <r>Children</r>.  The <s>SyncNodeBase.Path</s> of a node is the concatenation of all nodes starting from
/// the root (but not including the root, which has no name) down to and including the node itself,
/// with each path component separated by a backslash "\"; for example "Foo\Bar" is the path of the
/// node named "Bar" that's a child of the node named "Foo" that's a child of the root of the
/// SyncTree.
/// </para>
/// <para>
/// The purpose of the SyncTree is to allow a client to hold a snapshot of state that's stored on
/// the server.  A SyncNode is the granularity of synchronization between the client and the
/// server.  A client can listen for changes to a node's value (<r>SyncNode.ValueChanged</r>) or
/// changes among the nodes descendants (<r>SyncNode.DescendantChanged</r>).
/// </para>
/// <para>
/// A key feature of
/// the SyncTree is that the client can listen for changes to a node even before the first time
/// that the node was sent from the server.  For example, the client can listen for changes in the
/// value of "Foo\Bar" by creating the node "Foo\Bar" and hooking up to its
/// <s>SyncNode.ValueChanged</s> event, and then when the server sends "Foo\Bar" to the client
/// the event will be fired (assuming "Foo\Bar" has a non-null value at that point).  A side effect
/// of this features is that SyncTree doesn't directly support the concept of node deletion --
/// instead, it's up to individual controls (for example) to create a convention of node deletion
/// where needed.  One example is <n>TouchListBox</n>: its
/// <n>TouchListBox.ItemsRelativeSyncPath</n> property identifies a node whose children should be
/// displayed as list box items.  <n>TouchListBox</n> follows the convention that if a child has
/// a null <s>SyncNode.Value</s> and no <s>SyncNode.Children</s> then the child is ignored --
/// effectively deleted.
/// </para>
/// </remarks>
/// 
[DebuggerDisplay("SyncNode {Path} = {Value}")]
public class SyncNode : SyncNodeBase
{
    //////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    ISyncClient _syncClient;
    Dictionary<string, SyncNode> _children;
    object _value;
    SyncNodeValueType _valueType;
    int _pendingUpdatesCount;

    /// <summary>
    /// TODO
    /// </summary>
    public SyncNodeValueType ValueType
    {
        get
        {
            return _valueType;
        }
    }

    /// <summary>
    /// <n>true</n> if we're within a <r>SetValue</r> call.
    /// </summary>
    bool _inSetValue;


    //////////////////////////////////////////////////////////////////////////////////////////////
    // Public Properties
    //

    //string _name;
    //public override string Name
    //{
    //    get
    //    {
    //        return _name;
    //    }
    //}

    //SyncNodeBase _parent;
    //public override SyncNodeBase Parent
    //{
    //    get
    //    {
    //        return _parent;
    //    }
    //}

    /// <summary>
    /// Returns the value associated with this <r>SyncNode</r>.  Note that the value may be
    /// <n>null</n>.
    /// </summary>
    /// 
    /// <remarks>
    /// <r>Value</r> cannot be changed from within a <r>ValueChanged</r> event handler.
    /// </remarks>
    ///
    public override object Value
    {
        get
        {
            return _value;
        }
        set
        {
            SetValue(value, null, true);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    bool _isWaiting;

    /// <summary>
    /// Returns <n>true</n> if a value written to <n>Value</n> has not
    /// yet been reflected on the server.
    /// </summary>
    public override bool IsWaiting
    {
        get
        {
            return _isWaiting;
        }
    }

    //public override SyncPath Path
    //{
    //    get
    //    {
    //        return GetPath(this);
    //    }
    //}

    //public static SyncPath GetPath(SyncNodeBase syncNode)
    //{
    //    SyncNodeBase node = syncNode;
    //    int depth = 0;
    //    while (true)
    //    {
    //        node = node.Parent;
    //        if (node == null)
    //            break;
    //        depth++;
    //    }

    //    if (depth == 0)
    //        return SyncPath.Empty;

    //    // e.g. for "def\ghi", <depth> is 2 
    //    string[] parts = new string[depth];
    //    node = syncNode;
    //    while (true)
    //    {
    //        SyncNodeBase parent = node.Parent;
    //        if (parent == null)
    //            break;
    //        parts[--depth] = node.Name;
    //        node = parent;
    //    }

    //    return SyncPath.Build(parts);
    //}

    /// <summary>
    /// Returns an enumeration of the children of this <r>SyncNode</r>.  If there are no children,
    /// the enumeration returns no items.
    /// </summary>
    ///
    public override IEnumerable<SyncNodeBase> Children
    {
        get
        {
            if (_children == null)
                yield break;
            foreach (SyncNode node in _children.Values)
                yield return node;
        }
    }

    /// <summary>
    /// Returns <n>true</n> if this <r>SyncNode</r> has one or more children, i.e. if
    /// <r>Children</r> is not empty.
    /// </summary>
    ///
    public override bool HasChildren
    {
        get
        {
            return (_children != null) && (_children.Count > 0);
        }
    }

    public override SyncNodeBase this[SyncPath path]
    {
        // indexer
        get
        {
            SyncNodeBase node = TryFindNode(path);
            if (node == null)
                throw new KeyNotFoundException("Path not found: " + path);
            return node;
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Public Events
    //

    /// <summary>
    /// TODO
    /// </summary>
    public event ValueChangedEventHandler ValueChanged;

    /// <summary>
    /// TODO
    /// </summary>
    public event DescendantChangedEventHandler DescendantChanged;

    /// <summary>
    /// TODO
    /// </summary>
    public event IsWaitingChangedEventHandler IsWaitingChanged;

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Public Methods
    //

    public static SyncNode CreateTree(ISyncClient syncClient)
    {
        return new SyncNode(syncClient);
    }

    public override SyncNodeBase TryFindNode(SyncPath path)
    {
        path.EnsureZeroUpCount();
        SyncNode node = this;
        foreach (string part in path.Parts)
        {
            if (part.Length > 0)
            {
                if (node._children == null)
                    return null;
                SyncNode child;
                if (!node._children.TryGetValue(part, out child))
                    return null;
                node = child;
            }
        }
        return node;
    }

    public override SyncNodeBase CreateNode(SyncPath path)
    {
        path.EnsureZeroUpCount();
        if (path.Parts.Length == 0)
            throw new ArgumentException("Path cannot be blank", "path");
        SyncNode node = this;
        foreach (string pathComponent in path.Parts)
        {
            if (pathComponent.Length > 0)
            {
                SyncNode child;
                node.EnsureChildrenCollectionExists();
                if (!node._children.TryGetValue(pathComponent, out child))
                {
                    child = new SyncNode(_syncClient)
                    {
                        Name = pathComponent,
                        Parent = node
                    };
                    node._children.Add(pathComponent, child);
                }
                node = child;
            }
        }

        return node;
    }

    /// <summary>
    /// TODO
    /// </summary>
    /// <param name="newValue">TODO</param>
    /// <param name="originator">TODO</param>
    public override void SetValue(object newValue, object originator)
    {
        SetValue(newValue, originator, true);
    }

    public override void PerformItemAction(ItemAction itemAction, string itemData, object originator)
    {
#if SILVERLIGHT
        switch (itemAction)
        {
        case ItemAction.SetStringValue:
        case ItemAction.SetNumericValue:
        case ItemAction.SetBooleanValue:
            if (itemData != null)
                SetValue(Item.UntypedValueToTypedValue(itemAction, itemData), originator);
            break;
        case ItemAction.ToggleBooleanValue:
            if (_value is bool)
                SetValue(!((bool)_value), originator);
            break;
        case ItemAction.ExecuteVerb:
            if (itemData != null)
                ExecuteCommand(itemData);
            break;
        }
#else
        // TODO
#endif
    }

    public void ExecuteCommand(string command)
    {
        // do nothing if we're not online
        if (!_syncClient.IsOnline)
            return;

        // TODO: implement this:
        _syncClient.QueueSyncNodeCommandToSendToServer(this, command);

        IncrementPendingUpdatesCount();
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Internal Methods
    //

    internal void NullifySubtree()
    {
        SetValue(null, null, false);
        if (_children != null)
        {
            foreach (SyncNode child in _children.Values)
                child.NullifySubtree();
        }
    }

    internal void SetValue(object newValue, object originator, bool fromClient)
    {
        // do nothing if we're not online
        if (!_syncClient.IsOnline)
            return;

		// ignore SetValue call if we're being called recursively, e.g. as the result of
		// ValueChanged event
		if (_inSetValue)
			return;

		_inSetValue = true;
		try
		{

            // if the server disabled a driver, i.e. set all node values to null for that driver,
            // don't allow the client to overwrite a node value with a non-null value, since that
            // would have the effect of making the device appear to be re-enabled
            if (fromClient && (_value == null) && !Path.IsPersonalization)
            {
                Util.Trace("Rejected change to \"{0}\" because its current value is null -- that SyncNode is not defined, or the driver is disabled",
                    Path);
                return;
            }

            if (fromClient && (newValue == null))
            {
                // I'm just curious if the client ever sets a SyncNode value to null
                Debugger.Break();
            }

			SyncNodeValueType valueType;
			if (newValue != null)
			{
				Type type = newValue.GetType();
				TypeCode typeCode = Type.GetTypeCode(type);
				switch (typeCode)
				{
				case TypeCode.Boolean:
				case TypeCode.Int32:
				case TypeCode.Double:
				case TypeCode.String:
					valueType = (SyncNodeValueType)typeCode;
					break;
				default:
					throw new ArgumentException(
						String.Format("SyncNode.Value cannot be of type {0}", type));
				}
			}
			else
				valueType = SyncNodeValueType.Empty;

            //if (_valueChangedWasSuppressed)
            //    _valueChangedWasSuppressed = false; // see comments below
            //else
			if (Object.Equals(_value, newValue))
				return;

			object oldValue = _value;
			_value = newValue;
			_valueType = valueType;

			if (fromClient) // i.e. need to send to server
				_syncClient.QueueSyncNodeToSendToServer(this);

			// Inform listeners of this node of the value change
#if false
            //
            // NOTE: if this value is from the server and this SyncNode is still in the waiting
			// state (e.g. hourglass is displayed) we don't want to fire ValueChanged because of
			// this scenario:
			//
			//   Imagine the user is dragging a sound volume slider, but the audio hardware is
			//   slow to respond, so one or more volume changes get queued up.  So this is what
			//   the user sees: they drag the slider from (e.g.) top (100%) to bottom (0%), and
			//   then over the next couple of seconds they see the slider jump back up to 75%, then
			//   down to 50%, then down to 25%, then to 0% (for example).  This is disconcerting.
			//
			// We can fix this in each UI control, but instead this code adopts the following
			// rule:
			//
			//   "Don't fire ValueChanged until the SyncNodeUpdate that drops
			//   <_pendingUpdatesCount> back to zero"
			//
			// IMPORTANT: At that last SyncNodeUpdate, <_pendingUpdatesCount> must drop to 0
			// before this method (SetValue) is called!  See
            //
#endif
            //bool fire;
            //if (ValueChanged == null)
            //    fire = false;
            //else
            //if (fromClient)
            //    fire = true;
            //else
            //if (_pendingUpdatesCount == 0)
            //    fire = true;
            //else
            //{
            //    // We're suppressing firing ValueChanged due to the scenario described above;
            //    // we'll keep track of this fact, to deal with the following additional scenario:
            //    //
            //    //   In a previous call to SetValue (due to the sound volume slider moving to
            //    //   100%), <_value> was set to 1.0, but ValueChanged was not fired, for the
            //    //   reasons described above.  Now, this SetValue call sets <_pendingUpdatesCount>
            //    //   to zero -- but, without <_valueChangedWasSuppressed>, we'd never get this
            //    //   far because of the code above the checks that <_value> and <newValue> are
            //    //   different before proceeding.
            //    //
            //    fire = false;
            //    _valueChangedWasSuppressed = true;
            //}
			if ((ValueChanged != null)
                /*&& (fromClient || (_pendingUpdatesCount == 0))
                */)
            //if (fire)
			{
				ValueChanged(this, new ValueChangedEventArgs()
				{
					OldValue = oldValue,
					NewValue = newValue,
					Originator = originator
				});
			}

			// queue DescendantChanged events as applicable
			DescendantChanges changes = DescendantChanges.ChildValueChanged;
			for (SyncNode syncNode = (SyncNode)Parent; syncNode != null; syncNode = (SyncNode)syncNode.Parent)
			{
				if (syncNode.DescendantChanged != null)
					_syncClient.QueueDescendantChangedEvent(syncNode, changes, originator);
				changes = DescendantChanges.DeepNodeValueChanged;
			}

			if (fromClient) // i.e. sent to server
			{
                IncrementPendingUpdatesCount();
			}
		}
		finally
		{
			_inSetValue = false;
		}
    }

    void IncrementPendingUpdatesCount()
    {
        if (_pendingUpdatesCount++ == 0)
        {
            _isWaiting = true;
            if (IsWaitingChanged != null)
            {
                IsWaitingChanged(this, new IsWaitingChangedEventArgs()
                {
                    IsWaiting = true
                });
            }
        }
    }

    internal void OnServerReportedOperationCompletion(int originatedHere)
    {
        if (originatedHere <= 0)
            return;

        _pendingUpdatesCount -= originatedHere;
        if (_pendingUpdatesCount == 0)
        {
            _isWaiting = false;
            if (IsWaitingChanged != null)
            {
                IsWaitingChanged(this, new IsWaitingChangedEventArgs()
                {
                    IsWaiting = false
                });
            }
        }
    }

    internal void OnServerReportedValueChange(object newValue, int originatedHere)
    {
        bool stoppedWaiting = false;
        if (originatedHere > 0)
        {
            _pendingUpdatesCount -= originatedHere;
            if (_pendingUpdatesCount == 0)
                stoppedWaiting = true;
        }

		// IMPORTANT: See comments inside SetValue() body: On a SyncNodeUpdate that drops
		// <_pendingUpdatesCount> to 0, <_pendingUpdatesCount> must be set to 0 before SetValue
		// is called!

        if (stoppedWaiting)
        {
            _isWaiting = false;
            if (IsWaitingChanged != null)
            {
                IsWaitingChanged(this, new IsWaitingChangedEventArgs()
                {
                    IsWaiting = false
                });
            }
        }

        if (newValue != Message.FailedValue)
            SetValue(newValue, null, false);

    }

    internal void FireDescendantChangedEvent(SyncNode sender, DescendantChangedEventArgs e, object originator)
    {
        if (DescendantChanged != null)
            DescendantChanged(sender, e);
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Private Methods
    //

    SyncNode(ISyncClient syncClient)
    {
        // private use only -- CreateNode creates nodes
        _syncClient = syncClient;
    }

    /*
    void GetPath(StringBuilder result)
    {
        // e.g. "ASP\Kitchen" is returned for (Root) -> ASP -> Kitchen

        // if this is the root node, do nothing -- the root node's path is String.Empty
        if (Parent == null)
            return;

        // if the parent of this node is the root, the path is the name of this node
        if (Parent.Name == null)
        {
            result.Append(Name);
            return;
        }

        // otherwise, append this node's name to the parent's path, with a "\" separator
        ((SyncNode)Parent).GetPath(result);
        result.Append('\\');
        result.Append(Name);
    }
    */

    void EnsureChildrenCollectionExists()
    {
        if (_children == null)
            _children = new Dictionary<string, SyncNode>();
    }

    public new string Dump
    {
        // re-implemented here just to make it easier to get to during debugging
        get
        {
            return base.Dump;
        }
    }
}

[DebuggerDisplay("ReadOnlySyncNode {Path} = {Value}")]
public class ReadOnlySyncNode : SyncNodeBase
{
    //SyncNodeBase _parent;
    //string _name;
    object _value;
    Dictionary<string, ReadOnlySyncNode> _children;

    internal ReadOnlySyncNode(string name, object value)
    {
        Name = name;
        _value = value;
    }

    internal void AddChildNode(ReadOnlySyncNode childNode)
    {
        if (_children == null)
            _children = new Dictionary<string, ReadOnlySyncNode>();
        childNode.Parent = this;
        _children.Add(childNode.Name, childNode);
    }

    //public override string Name
    //{
    //    get
    //    {
    //        return _name;
    //    }
    //}

    //public override SyncNodeBase Parent
    //{
    //    get
    //    {
    //        return _parent;
    //    }
    //}

    public override object Value
    {
        get
        {
            return _value;
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public override bool IsWaiting
    {
        get
        {
            return false;
        }
    }

    //public override SyncPath Path
    //{
    //    get
    //    {
    //        return SyncNode.GetPath(this);
    //    }
    //}

    public override IEnumerable<SyncNodeBase> Children
    {
        get
        {
            if (_children == null)
                yield break;
            foreach (SyncNodeBase node in _children.Values)
                yield return node;
        }
    }

    public override bool HasChildren
    {
        get
        {
            return (_children != null) && (_children.Count > 0);
        }
    }

    public override SyncNodeBase this[SyncPath path]
    {
        // indexer
        get
        {
            SyncNodeBase node = TryFindNode(path);
            if (node == null)
                throw new KeyNotFoundException("Path not found: " + path);
            return node;
        }
    }

    public override SyncNodeBase TryFindNode(SyncPath path)
    {
        path.EnsureZeroUpCount();
        ReadOnlySyncNode node = this;
        foreach (string part in path.Parts)
        {
            if (part.Length > 0)
            {
                if (node._children == null)
                    return null;
                ReadOnlySyncNode child;
                if (!node._children.TryGetValue(part, out child))
                    return null;
                node = child;
            }
        }
        return node;
    }

    public override SyncNodeBase CreateNode(SyncPath path)
    {
        throw new NotImplementedException();
    }


    /// <summary>
    /// 
    /// </summary>
    /// <param name="newValue"></param>
    /// <param name="originator"></param>
    /// 
    /// <remarks>
    /// <r>SetValue</r> cannot be called from within a <r>ValueChanged</r> event handler.
    /// </remarks>
    /// 
    public override void SetValue(object newValue, object originator)
    {
        throw new NotImplementedException();
    }

    public override void PerformItemAction(ItemAction itemAction, string itemData, object originator)
    {
        throw new NotImplementedException();
    }

    public new string Dump
    {
        // re-implemented here just to make it easier to get to during debugging
        get
        {
            return base.Dump;
        }
    }
}

/// <summary>
/// Specifies the type of a value stored in <r>SyncNode.Value</r>.
/// </summary>
public enum SyncNodeValueType
{
    Empty = TypeCode.Empty,
    Boolean = TypeCode.Boolean,
    Int32 = TypeCode.Int32,
    Double = TypeCode.Double,
    String = TypeCode.String
}

/// <summary>
/// Specifies an action to be performed on a control's <r>SyncNode</r> when some action (e.g.
/// click) is performed on the control or a child element.
/// </summary>
///
public enum ItemAction
{
    /// <summary>
    /// Do nothing.
    /// </summary>
    None,

    /// <summary>
    /// Set <r>SyncNode.Value</r> to the ItemData string value associated with the control or element.
    /// </summary>
    SetStringValue,

    /// <summary>
    /// Set <r>SyncNode.Value</r> to the ItemData associated with the control or element,
    /// after converting ItemData to <n>Double</n>.
    /// </summary>
    SetNumericValue, // set <r>SyncNode.Value</r> (treated as a double) to the ItemData associated with the control or element.

    /// <summary>
    /// Set <r>SyncNode.Value</r> to the ItemData associated with the control or element,
    /// after converting ItemData to <n>Boolean</n>.
    /// </summary>
    SetBooleanValue,

    /// <summary>
    /// Toggle <r>SyncNode.Value</r> between <n>Boolean</n> values <n>true</n> and <n>false</n>.
    /// ItemData is not used.
    /// </summary>
    ToggleBooleanValue,

    /// <summary>
    /// Perform the verb specified by ItemData.
    /// </summary>
    ExecuteVerb,

    /// <summary>
    /// Navigate to the screen specified by ItemData.
    /// </summary>
    Navigate
}

/// <summary>
/// TODO
/// </summary>
public class IsWaitingChangedEventArgs
{
    /// <summary>
    /// TODO
    /// </summary>
    public bool IsWaiting { get; internal set; }
}

/// <summary>
/// TODO
/// </summary>
public class IsOnlineChangedEventArgs
{
    /// <summary>
    /// TODO
    /// </summary>
    public bool IsOnline { get; internal set; }
}

/// <summary>
/// TODO
/// </summary>
/// <param name="sender">TODO</param>
/// <param name="e">TODO</param>
public delegate void IsWaitingChangedEventHandler(object sender, IsWaitingChangedEventArgs e);

/// <summary>
/// TODO
/// </summary>
/// <param name="sender">TODO</param>
/// <param name="e">TODO</param>
public delegate void IsOnlineChangedEventHandler(object sender, IsOnlineChangedEventArgs e);

/// <summary>
/// TODO
/// </summary>
/// <param name="sender">TODO</param>
/// <param name="e">TODO</param>
public delegate void DescendantChangedEventHandler(object sender, DescendantChangedEventArgs e);

/// <summary>
/// TODO
/// </summary>
public abstract class SyncNodeBase
{
    /// <summary>
    /// TODO; not that Name is null only for root node
    /// </summary>
    public string Name { get; protected set; }

    /// <summary>
    /// TODO
    /// </summary>
    public SyncNodeBase Parent { get; protected set; }

    /// <summary>
    /// TODO
    /// </summary>
    public abstract object Value { get; set; }

    /// <summary>
    /// TODO
    /// </summary>
    public abstract bool IsWaiting { get; }

    /// <summary>
    /// Returns the top-level node whose subtree contains this node.  Returns this node if it's a
    /// top-level node or the root node.  A top-level node is one whose parent is the root node.
    /// </summary>
    public SyncNodeBase TopLevelNode
    {
        get
        {
            SyncNodeBase node = this;
            while (true)
            {
                if ((node.Parent == null) || (node.Parent.Name == null))
                    return node;
                node = node.Parent;
            }
        }
    }

    /// <summary>
    /// TODO - returns SyncPath.Empty for root node, otherwise returns path of this node
    /// </summary>
    public SyncPath Path
    {
        get
        {
            SyncNodeBase node = this;
            int depth = 0;
            while (true)
            {
                node = node.Parent;
                if (node == null)
                    break;
                depth++;
            }

            if (depth == 0)
                return SyncPath.Empty;

            // e.g. for "def\ghi", <depth> is 2 
            string[] parts = new string[depth];
            node = this;
            while (true)
            {
                SyncNodeBase parent = node.Parent;
                if (parent == null)
                    break;
                parts[--depth] = node.Name;
                node = parent;
            }

            return SyncPath.Build(parts);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    public abstract IEnumerable<SyncNodeBase> Children { get; }
    
    /// <summary>
    /// TODO
    /// </summary>
    public abstract bool HasChildren { get; }

    /// <summary>
    /// TODO
    /// </summary>
    /// <param name="path">TODO</param>
    ///
    public abstract SyncNodeBase this[SyncPath path] { get; }
    
    /// <summary>
    /// TODO
    /// </summary>
    /// 
    /// <param name="path">TODO</param>
    ///
    public abstract SyncNodeBase TryFindNode(SyncPath path);

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="path">TODO</param>
    ///
    public abstract SyncNodeBase CreateNode(SyncPath path);

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="newValue">TODO</param>
    ///
    /// <param name="originator">TODO</param>
    ///
    public abstract void SetValue(object newValue, object originator);

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="itemAction">TODO</param>
    ///
    /// <param name="itemData">TODO</param>
    ///
    /// <param name="originator">TODO</param>
    ///
    public abstract void PerformItemAction(ItemAction itemAction, string itemData, object originator);

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <typeparam name="T">TODO</typeparam>
    ///
    /// <param name="defaultValue">TODO</param>
    ///
    public T TryGetValue<T>(T defaultValue)
    {
        try
        {
            if (Value == null)
                return defaultValue;
            else
                return (T)Value;
        }
        catch (InvalidCastException)
        {
            return defaultValue;
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    /// 
    /// <typeparam name="T">TODO</typeparam>
    /// 
    /// <param name="path">TODO</param>
    /// 
    /// <param name="defaultValue">TODO</param>
    ///
    public T TryFindValue<T>(SyncPath path, T defaultValue)
    {
        SyncNodeBase syncNode = TryFindNode(path);
        if (syncNode != null)
            return syncNode.TryGetValue<T>(defaultValue);
        else
            return defaultValue;
    }

    /// <summary>
    /// TODO
    /// </summary>
    /// 
    /// <typeparam name="T">TODO</typeparam>
    /// 
    /// <param name="path">TODO</param>
    /// 
    /// <param name="defaultValue">TODO</param>
    ///
    public T TryFindValue<T>(string path, T defaultValue)
    {
        return TryFindValue(SyncPath.FromString(path), defaultValue);
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public string Dump
    {
        get
        {
            StringBuilder builder = new StringBuilder(1000);
            GetDump(builder, 0);
            return builder.ToString();
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="builder">TODO</param>
    ///
    /// <param name="indent">TODO</param>
    ///
    void GetDump(StringBuilder builder, int indent)
    {
        if (Name != null)
        {
            builder.AppendFormat("{0}{1}", new String(' ', indent * 4), Name);
            if (Value != null)
            {
                string stringValue;
                if ((stringValue = Value as string) != null)
                    builder.AppendFormat(" = \"{0}\"\n", stringValue);
                else
                    builder.AppendFormat(" = {0}\n", Value);
            }
            else
            {
                if (HasChildren)
                    builder.Append("\n");
                else
                    builder.Append(" = null\n");
            }
        }
        foreach (SyncNodeBase node in Children)
            node.GetDump(builder, indent + ((Name == null) ? 0 : 1));
    }
}

/// <summary>
/// Specifies which changes changes occurred to descendants of a given <r>SyncNode</r>.
/// </summary>
///
/// <remarks>
/// <r>DescendantChanges</r> identify changes to the SyncTree on this client, not the SyncTree as
/// maintained on the server.  For example, <r>ChildAdded</r> means either "someone on this client
/// added a child" or "this client just received notification that a child exists that we didn't
/// know about" -- in the latter case, the child may have already existed but (for example) this
/// client may have just started executing and so it just found out about the child.
/// </remarks>
///
[Flags]
public enum DescendantChanges
{
    /// <summary>
    /// One or more child node(s) were added to the global SyncTree on this client.
    /// </summary>
    ChildAdded = 1,

    /// <summary>
    /// The value of one or more child node(s) changed in the global SyncTree on this client.
    /// </summary>
    ChildValueChanged = 2,

    /// <summary>
    /// One or more descendant(s) of child node(s) were added to the global SyncTree on this client.
    /// </summary>
    DeepNodeAdded = 4,

    /// <summary>
    /// The value of one or more descendant(s) of child node(s) changed in the global SyncTree on this client.
    /// </summary>
    DeepNodeValueChanged = 8,
}

/// <summary>
/// Arguments for <r>DescendantChangedEventHandler</r>.  For example, see
/// <r>SyncNode.DescendantChanged</r>.
/// </summary>
///
public class DescendantChangedEventArgs : EventArgs
{
    /// <summary>
    /// Specifies which changes changes occurred to descendants of the given <r>SyncNode</r>.
    /// </summary>
    public DescendantChanges Changes { get; internal set; }

    /// <summary>
    /// Specifies the object that made the change that caused this event to be fired.  If more than
    /// one object made changes that led to this event, or if the originator is not known, not
    /// specified, or not applicable, <r>Originator</r> is <n>null</n>.
    /// </summary>
    public object Originator { get; internal set; }
}

/// <summary>
/// TODO
/// </summary>
/// <param name="sender">TODO</param>
/// <param name="e">TODO</param>
public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);

/// <summary>
/// TODO
/// </summary>
public class ValueChangedEventArgs : EventArgs
{
    public object OldValue { get; internal set; }
    public object NewValue { get; internal set; }

    /// <summary>
    /// Specifies the object that made the change that caused this event to be fired, or
    /// <n>null</n> if the originator is not known, not specified, or not applicable.
    /// </summary>
    public object Originator { get; internal set; }
}

public delegate void NewValueEventHandler<T>(object sender, NewValueEventArgs<T> e);

public class NewValueEventArgs<T>
{
    public T NewValue { get; private set; }

    public NewValueEventArgs(T newValue)
    {
        NewValue = newValue;
    }
}

// like ParsedCommand, but throws SyncException instead of FormatException
public class ParsedSyncNodeCommand : ParsedCommand
{
    public ParsedSyncNodeCommand(string stringForm)
        : base(stringForm)
    {
    }

    protected override Exception NewFormatException(string format, params object[] args)
    {
        return new SyncException(format, args);
    }
}

/// <summary>
/// TODO
/// </summary>
public class SyncException : Exception
{
    public SyncException(Exception innerException, string format, params object[] args) :
        base(Util.Format(format, args), innerException)
    {
    }

    public SyncException(string format, params object[] args) :
        base(Util.Format(format, args))
    {
    }

    /// <summary>
    /// Initializes an instance of this class.  The error message indicates that a given
    /// <r>SyncPath</r> is invalid.
    /// </summary>
    /// <param name="syncPath"></param>
    /// <param name="format"></param>
    /// <param name="args"></param>
    public SyncException(SyncPath syncPath, string format, params object[] args) :
        this(String.Format("Invalid SyncPath \"{0}\": {1}", syncPath, format), args)
    {
    }

    /// <summary>
    /// Initializes an instance of this class.  The error message indicates that there's a problem
    /// with a given <r>SyncNodeUpdate</r>.
    /// </summary>
    /// <param name="update"></param>
    /// <param name="format"></param>
    /// <param name="args"></param>
    public SyncException(SyncNodeUpdate update, string format, params object[] args) :
        this(String.Format("Problem with update of SyncNode \"{0}\": {1}", update.Path, format), args)
    {
    }

    /// <summary>
    /// Initializes an instance of this class.  The error message indicates that there's a problem
    /// with a given <r>SyncNodeCommand</r>.
    /// </summary>
    /// <param name="command"></param>
    /// <param name="format"></param>
    /// <param name="args"></param>
    public SyncException(SyncNodeCommand command, string format, params object[] args) :
        this(String.Format("Problem with command on SyncNode \"{0}\": {1}", command.Path, format), args)
    {
    }
}

}

